﻿using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Simplification;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>""</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class EmptyStringLiteralDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        #region Description
        public const string DiagnosticId = "EmptyStringLiteral";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticId,
            "Empty string literal",
            "Use String.Empty as empty string.",
            "Usage",
            DiagnosticSeverity.Warning
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest { get { return ImmutableArray.Create(SyntaxKind.StringLiteralExpression); } }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
        #endregion

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var literal = node as LiteralExpressionSyntax) != null)
            if (literal.Token.ValueText == String.Empty)
            {
                var ancestors = literal.Token.Parent.AncestorsAndSelf().ToList();

                // exclude, when used as a constant in a parameter
                if (ancestors.Any(n => n.IsKind(SyntaxKind.Parameter) || n.IsKind(SyntaxKind.AttributeArgument)))
                    return;

                // exclude, when used as a constant in an initializer
                if ((var field = ancestors.OfType<FieldDeclarationSyntax>().FirstOrDefault()) != null)
                if (field.Modifiers.Any(SyntaxKind.ConstKeyword))
                    return;

                if ((var local = ancestors.OfType<LocalDeclarationStatementSyntax>().FirstOrDefault()) != null)
                if (local.IsConst)
                    return;

                addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
            }
        }
    }

    [ExportCodeFixProvider(EmptyStringLiteralDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class EmptyStringLiteralCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            yield return EmptyStringLiteralDiagnosticAnalyzer.DiagnosticId;
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // find the literal identified by the diagnostic.
            var literal = root.FindToken(diagnosticSpan.Start).Parent.FirstAncestorOrSelf<LiteralExpressionSyntax>();

            return new[] { CodeAction.Create("Use String.Empty", c => Fix(root, document, literal, c)) };
        }

        private async Task<Document> Fix(SyntaxNode root, Document document, LiteralExpressionSyntax literal, CancellationToken cancellationToken)
        {
            // replace node
            var newNode = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                SyntaxFactory.ParseName("System.String"), SyntaxFactory.IdentifierName("Empty")
            )
                .WithAdditionalAnnotations(Simplifier.Annotation)
            ;

            // add using to System
            SyntaxNode newRoot = root.ReplaceNode<SyntaxNode, ExpressionSyntax>(literal, newNode);
            newRoot = (newRoot as CompilationUnitSyntax).WithUsing("System");

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
